package biz.datalk.industrialland.admin.modules.common.servie.impl;

import biz.datalk.commons.utils.BasicParserUtils;
import biz.datalk.industrialland.admin.config.properties.CaptchaProperties;
import biz.datalk.industrialland.admin.modules.common.pojo.dto.CaptchaDTO;
import biz.datalk.industrialland.admin.modules.common.pojo.vo.CaptchaVO;
import biz.datalk.industrialland.admin.modules.common.servie.ICaptchaService;
import biz.datalk.industrialland.common.def.enums.DBYNEnum;
import biz.datalk.industrialland.common.exception.ServiceException;
import biz.datalk.industrialland.common.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.Optional;
import java.util.Random;

/**
 * @author tarofang@163.com
 * @date 2023-09-05
 */
@Slf4j
@Service
public class CaptchaServiceImpl implements ICaptchaService {
    public static final int COUNT = 200;
    public static final int WIDTH = 105;
    public static final int HEIGHT = 35;
    public static final int LINE_WIDTH = 2;
    public static final int DEFAULT_CAPTCHA_LENGTH = 4;
    /** 备选字符串 */
    public static final String DEFAULT_CAPTCHA_SEED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    public static final String CAPTCHA_CODE_PREFIX = "captcha:";
    /** 验证码长度 */
    public static final String SYS_CONFIG_CAPTCHA_LENGTH = "sys.config.captcha.length";
    /** 验证码有效时间 , 单位: 秒 */
    public static final String SYS_CONFIG_CAPTCHA_TIMEOUT = "sys.config.captcha.timeout";
    /** 验证码备选字符串 */
    public static final String SYS_CONFIG_CAPTCHA_SEED = "sys.config.captcha.seed";
    private static final String IMG_BASE64_PREFIX = "data:image/jpg;base64,";

    private final CaptchaProperties captchaProperties;
    private final RedisUtil redisUtil;

    @Autowired
    public CaptchaServiceImpl(
            CaptchaProperties captchaProperties, RedisUtil redisUtil) {
        this.captchaProperties = captchaProperties;
        this.redisUtil = redisUtil;
    }

    @Override
    public CaptchaVO create(CaptchaDTO captchaDTO) {
        return this.doCreate(captchaDTO);
    }

    @Override
    public boolean checkRandCode(String inputCode, String ran) {
        String cacheKey = this.getRandCodeCacheKey(ran);
        String cacheVal = redisUtil.getStr(cacheKey);
        boolean checkResult = captchaProperties.getCheckIgnoreCase() //
                ? StringUtils.equalsIgnoreCase(cacheVal, inputCode) //
                : StringUtils.equals(cacheVal, inputCode);
        if (log.isDebugEnabled()) {
            log.debug("[checkResult={}, inputCode={}, cacheVal={}]", checkResult, inputCode, cacheVal);
        }
        return checkResult;
    }

    @Override
    public boolean checkRandCode(String inputCode, String ran, boolean delete) {
        boolean checkResult = this.checkRandCode(inputCode, ran);
        if (delete) {
            String cacheKey = this.getRandCodeCacheKey(ran);
            redisUtil.del(cacheKey);
        }
        return checkResult;
    }

    private String getRandCodeCacheKey(String ran) {
        return CAPTCHA_CODE_PREFIX + ran;
    }


    public CaptchaVO doCreate(CaptchaDTO captchaDTO) {
        log.debug("doCreate into");
        Integer width = captchaDTO.getWidth();
        width = Optional.ofNullable(width).orElse(0);
        width = (width <= 80 || width > 800) ? WIDTH : width;
        Integer height = captchaDTO.getHeight();
        height = Optional.ofNullable(height).orElse(0);
        height = (height <= 30 || height > 800) ? HEIGHT : height;
        String hasImgPrefixStr = captchaDTO.getHasImgPrefix();
        boolean hasImgPrefix = StringUtils.isBlank(hasImgPrefixStr) ? Boolean.TRUE : StringUtils.equals(hasImgPrefixStr.trim(), DBYNEnum.Y.name());

        String codeSeed = StringUtils.trimToNull(captchaProperties.getCodeSeed());
        Integer codeLen = captchaProperties.getCodeLen();
        codeLen = codeLen != null && codeLen > 0 ? codeLen : null;

        int captchaLen;
        if (codeLen != null) {
            captchaLen = codeLen;
        } else {
            String captchaLenStr = redisUtil.getStr(SYS_CONFIG_CAPTCHA_LENGTH);
            captchaLen = BasicParserUtils.parseInt(captchaLenStr, DEFAULT_CAPTCHA_LENGTH);
        }

        String captchaSeed;
        if (StringUtils.isNotBlank(codeSeed)) {
            captchaSeed = codeSeed;
        } else {
            captchaSeed = redisUtil.getStr(SYS_CONFIG_CAPTCHA_SEED);
            captchaSeed = StringUtils.isBlank(captchaSeed) ? DEFAULT_CAPTCHA_SEED : captchaSeed.trim();
        }

        final String captcha = genCaptchaCode(captchaSeed, captchaLen);
        log.debug("captcha: {}", captcha);

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            BufferedImage image = this.generageCaptchaImage(captcha, width, height);
            ImageIO.write(image, "JPEG", outputStream);
            outputStream.flush();

            String base64jpg = Base64Utils.encodeToString(outputStream.toByteArray());
            if (hasImgPrefix) {
                base64jpg = IMG_BASE64_PREFIX.concat(base64jpg);
            }

            String ran = RandomStringUtils.randomAlphabetic(6);
            long nano = System.nanoTime();
            ran += nano;
            log.debug("captcha ran: {}", ran);
            String key = CAPTCHA_CODE_PREFIX + ran;
            // 过期时间 秒
            String timeoutStr = redisUtil.getStr(SYS_CONFIG_CAPTCHA_TIMEOUT);
            long expireSeconds = captchaProperties.getExpire().getSeconds();
            long expire = BasicParserUtils.parseLong(timeoutStr, expireSeconds);
            redisUtil.set(key, captcha, expire);

            CaptchaVO codeImageVO = new CaptchaVO();
            codeImageVO.setJpg(base64jpg);
            codeImageVO.setRan(ran);

            return codeImageVO;
        } catch (Exception ex) {
            log.error("ERR: {}", ex.getMessage(), ex);
            throw new ServiceException("验证码生成失败");
        } finally {
            log.debug("doCreate out");
        }
    }


    private BufferedImage generageCaptchaImage(String captcha, int width, int height) {
        // 在内存中创建图象
        final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        // 获取图形上下文
        final Graphics2D graphics = (Graphics2D) image.getGraphics();

        // 设定背景颜色
        Color bgColor = getRandColor(180, 240);
        graphics.setColor(bgColor);
        graphics.fillRect(0, 0, width, height);

        // 设定边框颜色
        graphics.drawRect(0, 0, width - 1, height - 1);

        final Random random = new Random();
        // 随机产生干扰线，使图象中的认证码不易被其它程序探测到
        for (int i = 0; i < COUNT; i++) {
            graphics.setColor(getRandColor(150, 200));

            // 保证画在边框之内
            final int x = random.nextInt(width - LINE_WIDTH - 1) + 1;
            final int y = random.nextInt(height - LINE_WIDTH - 1) + 1;
            final int xl = random.nextInt(LINE_WIDTH);
            final int yl = random.nextInt(LINE_WIDTH);
            graphics.drawLine(x, y, x + xl, y + yl);
        }

        int fontSize = height * 24 / HEIGHT;
        int y = height * 26 / HEIGHT;
        int x1 = width * 23 / WIDTH;
        int x2 = width * 8 / WIDTH;


        // 绘制验证码
        for (int i = 0, len = captcha.length(); i < len; i++) {
            // 将认证码显示到图象中,调用函数出来的颜色相同，可能是因为种子太接近，所以只能直接生成
            // graphics.setColor(new Color(20 + random.nextInt(130), 20 + random
            // .nextInt(130), 20 + random.nextInt(130)));

            // 设置字体颜色
            graphics.setColor(Color.BLACK);

            // 设置字体样式
            // graphics.setFont(new Font("Arial Black", Font.ITALIC, 18));
            // graphics.setFont(new Font("Times New Roman", Font.BOLD, 24));
            graphics.setFont(new Font("Times New Roman", Font.BOLD, fontSize));

            // 设置字符，字符间距，上边距
            // graphics.drawString(String.valueOf(captcha.charAt(i)), (23 * i) + 8, 26);
            graphics.drawString(String.valueOf(captcha.charAt(i)), (x1 * i) + x2, y);
        }

        // 图象生效
        graphics.dispose();

        return image;
    }

    private BufferedImage generageCaptchaImage(String captcha) {
        // 在内存中创建图象
        final BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);

        // 获取图形上下文
        final Graphics2D graphics = (Graphics2D) image.getGraphics();

        // 设定背景颜色
        Color bgColor = getRandColor(180, 240);
        graphics.setColor(bgColor);
        graphics.fillRect(0, 0, WIDTH, HEIGHT);

        // 设定边框颜色
        graphics.drawRect(0, 0, WIDTH - 1, HEIGHT - 1);

        final Random random = new Random();
        // 随机产生干扰线，使图象中的认证码不易被其它程序探测到
        for (int i = 0; i < COUNT; i++) {
            graphics.setColor(getRandColor(150, 200));

            // 保证画在边框之内
            final int x = random.nextInt(WIDTH - LINE_WIDTH - 1) + 1;
            final int y = random.nextInt(HEIGHT - LINE_WIDTH - 1) + 1;
            final int xl = random.nextInt(LINE_WIDTH);
            final int yl = random.nextInt(LINE_WIDTH);
            graphics.drawLine(x, y, x + xl, y + yl);
        }

        // 绘制验证码
        for (int i = 0, len = captcha.length(); i < len; i++) {
            // 将认证码显示到图象中,调用函数出来的颜色相同，可能是因为种子太接近，所以只能直接生成
            // graphics.setColor(new Color(20 + random.nextInt(130), 20 + random
            // .nextInt(130), 20 + random.nextInt(130)));

            // 设置字体颜色
            graphics.setColor(Color.BLACK);

            // 设置字体样式
            // graphics.setFont(new Font("Arial Black", Font.ITALIC, 18));
            graphics.setFont(new Font("Times New Roman", Font.BOLD, 24));

            // 设置字符，字符间距，上边距
            graphics.drawString(String.valueOf(captcha.charAt(i)), (23 * i) + 8, 26);
        }

        // 图象生效
        graphics.dispose();

        return image;
    }

    private Color getRandColor(int fc, int bc) { // 取得给定范围随机颜色
        final Random random = new Random();
        int max = 255;
        if (fc > max) {
            fc = max;
        }
        if (bc > max) {
            bc = max;
        }

        final int r = fc + random.nextInt(bc - fc);
        final int g = fc + random.nextInt(bc - fc);
        final int b = fc + random.nextInt(bc - fc);

        return new Color(r, g, b);
    }

    @SuppressWarnings("StringBufferMayBeStringBuilder")
    private String genCaptchaCode(String seed, int len) {
        final Random random = new Random();
        final StringBuffer sb = new StringBuffer();
        int length = seed.length();
        for (int i = 0; i < len; i++) {
            sb.append(seed.charAt(random.nextInt(length)));
        }
        return sb.toString();
    }
}
